package com.mega.fe.util;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

@SuppressWarnings("unused")
public class UnsafeUtil {
    private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
    private static final long COPY_STRIDE = 8;
    public Object o;

    public UnsafeUtil(Object o) {
        this.o = o;
    }

    public static boolean isFieldStatic(Field field) {
        return (field.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0;
    }

    public static long offset(Field field) {
        try {
            return isFieldStatic(field) ? MyUnsafe.staticFieldOffset(field) : MyUnsafe.objectFieldOffset(field);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public void set(Field field, Object x) {
        //UnsafeHacks.setField(field, o, x);
        try {
            MyUnsafe.putObject(o, offset(field), x);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public Object get(Field field) {
        //return UnsafeHacks.getField(field, o);
        try {
            return MyUnsafe.getObject(o, offset(field));
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public void warp(Object target, Field field) {
        UnsafeUtil unsafeUtil = new UnsafeUtil(target);
        Object valueBase = get(field);
        Object valueTarget = unsafeUtil.get(field);
        set(field, valueTarget);
        unsafeUtil.set(field, valueBase);
    }

    public void replace(Object target, Field field) {
        UnsafeUtil unsafeUtil = new UnsafeUtil(target);
        Object valueTarget = unsafeUtil.get(field);
        set(field, valueTarget);
    }

    public void overrideSelfFields(Object target) {
        UnsafeUtil unsafeUtil = new UnsafeUtil(target);
        for (Field field : target.getClass().getDeclaredFields()) {
            System.out.println(field);
            set(field, unsafeUtil.get(field));
        }
    }
    @SuppressWarnings("unchecked")
    public static void setClassLoader(Class<?> klass, ClassLoader loader) throws Throwable {
        VarHandle classLoaderVar = MEGA.IMPL_LOOKUP().findVarHandle(Class.class, "classLoader", ClassLoader.class);
        classLoaderVar.set(klass, loader);
        VarHandle classesVar = MEGA.IMPL_LOOKUP().findVarHandle(ClassLoader.class, "classes", ArrayList.class);
        ArrayList<Class<?>> list = (ArrayList<Class<?>>) classesVar.get(loader);
        list.add(klass);
        classesVar.set(loader, list);
    }

    public static void setLoaderBoot(Class<?> klass) {
        try {
            setClassLoader(klass, MEGA.DefineClassLoader.BOOT_LOADER);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public static void cleanClass(Class<?> sharky) throws NoSuchFieldException, IllegalAccessException {
        VarHandle classesVar = MEGA.IMPL_LOOKUP().findVarHandle(ClassLoader.class, "classes", ArrayList.class);
        VarHandle classLoaderVar = MEGA.IMPL_LOOKUP().findVarHandle(Class.class, "classLoader", ClassLoader.class);
        ClassLoader loader = sharky.getClassLoader();
        classLoaderVar.set(sharky, null);
        while (loader.getParent() != null && classesVar.get(loader) != null) {
            @SuppressWarnings("unchecked")
            ArrayList<Class<?>> list = (ArrayList<Class<?>>) classesVar.get(loader);
            for (int i = 0;i < list.size();i++) {
                Class<?> k = list.get(i);
                System.out.println(" classes check:" + k.getName());
                if (k.getName().equals(sharky.getName())) {
                    System.out.printf("!!CHECK SUCCESSFULLY !!, now the classLoader:%s, name:\"%s%n\"", loader.getClass().getName(), loader.getName());
                    System.out.printf("And the targetClass'ClassLoader is :%s%n", sharky.getClassLoader());
                    ((ArrayList<Class<?>>)classesVar.get(MEGA.DefineClassLoader.BOOT_LOADER)).remove(sharky);
                    list.remove(k);
                }
            }
            classesVar.set(loader, list);
            loader = loader.getParent();
        }
    }

    public static Class<?> INTERNAL_UNSAFE_CLASS() throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        return MEGA.IMPL_LOOKUP().findClass("jdk.internal.misc.Unsafe");
    }

    public static Object INTERNAL_UNSAFE_INSTANCE() throws Throwable {
        MethodHandle v = MEGA.IMPL_LOOKUP().findStatic(INTERNAL_UNSAFE_CLASS(), "getUnsafe", MethodType.methodType(INTERNAL_UNSAFE_CLASS()));
        return v.invoke();
    }

    public static boolean isAccessible(Class<?> caller)  {
        return (caller.getName().startsWith("com.mega") || caller.getName().startsWith("jdk.") || caller.getName().startsWith("java.lang.") || caller.getName().startsWith("sun.")) ;

    }

    /**
     * Returns the address the object is located at
     *
     * <p>WARNING: This does not return a pointer, so be warned pointer arithmetic will not work.
     *
     * @param obj The object
     * @return the address of the object
     */
    public static long toAddress(Object obj) throws Throwable {
        Object[] array = new Object[] {obj};
        long baseOffset = MyUnsafe.arrayBaseOffset(Object[].class);
        return normalize(MyUnsafe.getInt(array, baseOffset));
    }

    /**
     * Returns the object located at the address.
     *
     * @param address The address
     * @return the object at this address
     */
    public static Object fromAddress(long address) throws Throwable {
        Object[] array = new Object[] {null};
        long baseOffset = MyUnsafe.arrayBaseOffset(Object[].class);
        MyUnsafe.putLong(array, baseOffset, address);
        return array[0];
    }

    public static void copyMemory(long srcAddress, Object dest) throws Throwable {
        copyMemory(null, srcAddress, dest, 0, sizeOf(dest));
    }

    /**
     * Copies the memory from srcAddress into dest
     *
     * <p>This is our own implementation because Unsafe.copyMemory(Object src, .. Object dest, ...)
     * only works if <a href="https://goo.gl/pBVlJv">dest in an array</a>, so we wrote our only
     * implementations.
     */
    public static void copyMemory(final Object src, long srcOffset, final Object dest,
                                  final long destOffset, final long len) throws Throwable {

        // TODO make this work if destOffset is not STRIDE aligned
        //Preconditions.checkNotNull(src);
        Preconditions.checkArgument(len % COPY_STRIDE != 0, "Length (%d) is not a multiple of stride", len);

        Preconditions.checkArgument(destOffset % COPY_STRIDE != 0,
                "Dest offset (%d) is not stride aligned", destOffset);

        long end = destOffset + len;
        for (long offset = destOffset; offset < end; ) {
            MyUnsafe.putLong(dest, offset, MyUnsafe.getLong(null, srcOffset));
            offset += COPY_STRIDE;
            srcOffset += COPY_STRIDE;
        }
    }

    /**
     * Copies from srcAddress to dest one field at a time.
     *
     * @param srcAddress
     * @param dest
     */
    public static void copyMemoryFieldByField(long srcAddress, Object dest) throws Throwable {

        Class<?> clazz = dest.getClass();
        while (clazz != Object.class) {
            for (Field f : clazz.getDeclaredFields()) {
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    final Class<?> type = f.getType();

                    // TODO maybe support Wrapper classes
                    Preconditions.checkArgument(type.isPrimitive(), "Only primitives are supported");

                    final long offset = MyUnsafe.objectFieldOffset(f);
                    final long src = srcAddress + offset;

                    if (type == int.class) {
                        MyUnsafe.putInt(dest, offset, MyUnsafe.getInt(null, src));

                    } else if (type == long.class) {
                        MyUnsafe.putLong(dest, offset, MyUnsafe.getLong(null, src));

                    } else {
                        throw new IllegalArgumentException("Type not supported yet: " + type);
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    public static long jvm7_32_sizeOf(Object object) throws Throwable {
        // This is getting the size out of the class header (at offset 12)
        return MyUnsafe.getAddress(null, normalize(MyUnsafe.getInt(object, 4L)) + 12L);
    }

    public static long headerSize(Object obj) {
        return headerSize(obj.getClass());
    }

    public static long firstFieldOffset(Object obj) throws Throwable {
        return firstFieldOffset(obj.getClass());
    }

    @SuppressWarnings("TypeParameterExplicitlyExtendsObject")
    public static long sizeOf(Object obj) throws Throwable {
        Class<? extends Object> clazz = obj.getClass();

        long len = sizeOf(clazz);

        if (clazz.isArray()) {
            // TODO Do extra work
            // TODO move into sizeof(Object)
            // (8) first longs and doubles; then
            // (4) ints and floats; then
            // (2) chars and shorts; then
            // (1) bytes and booleans, and last the
            // (4-8) references.
            Object[] array = (Object[]) obj;
            len += array.length * 8L;
        }

        return len;
    }

    public static long sizeOfFields(Object obj) throws Throwable {
        return sizeOfFields(obj.getClass());
    }

    private static long roundUpTo8(final long number) {
        return ((number + 7) / 8) * 8;
    }

    /**
     * Returns the size of the header for an instance of this class (in bytes).
     *
     * <p>More information <a href="http://www.codeinstructions.com/2008/12/java-objects-memory-structure.html">http://www.codeinstructions.com/2008/12/java-objects-memory-structure.html</a>
     * and <a href="http://stackoverflow.com/a/17348396/88646">http://stackoverflow.com/a/17348396/88646</a>
     *
     * <p><pre>
     * ,------------------+------------------+------------------ +---------------.
     * |    mark word(8)  | klass pointer(4) |  array size (opt) |    padding    |
     * `------------------+------------------+-------------------+---------------'
     * </pre>
     *
     * @param clazz
     * @return
     */
    public static long headerSize(Class<?> clazz) {
        Preconditions.checkNotNull(clazz);
        // TODO Should be calculated based on the platform
        // TODO maybe unsafe.addressSize() would help?
        long len = 12; // JVM_64 has a 12 byte header 8 + 4 (with compressed pointers on)
        if (clazz.isArray()) {
            len += 4;
        }
        return len;
    }

    /**
     * Returns the offset of the first field in the range [headerSize, sizeOf].
     *
     * @param clazz
     * @return
     */
    public static long firstFieldOffset(Class<?> clazz) throws Throwable {
        long minSize = roundUpTo8(headerSize(clazz));

        // Find the min offset for all the classes, up the class hierarchy.
        while (clazz != Object.class) {
            for (Field f : clazz.getDeclaredFields()) {
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    long offset = MyUnsafe.objectFieldOffset(f);
                    if (offset < minSize) {
                        minSize = offset;
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }

        return minSize;
    }

    /**
     * Returns the size of an instance of this class (in bytes).
     * Instances include a header + all fields + padded to 8 bytes.
     * If this is an array, it does not include the size of the elements.
     *
     * @param clazz
     * @return
     */
    public static long sizeOf(Class<?> clazz) throws Throwable {
        long maxSize = headerSize(clazz);

        while (clazz != Object.class) {
            for (Field f : clazz.getDeclaredFields()) {
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    long offset = MyUnsafe.objectFieldOffset(f);
                    if (offset > maxSize) {
                        // Assume 1 byte of the field width. This is ok as it gets padded out at the end
                        maxSize = offset + 1;
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }

        // The whole class always pads to a 8 bytes boundary, so we round up to 8 bytes.
        return roundUpTo8(maxSize);
    }

    /**
     * Size of all the fields
     * @param clazz
     * @return
     */
    public static long sizeOfFields(Class<?> clazz) throws Throwable {
        return sizeOf(clazz) - firstFieldOffset(clazz);
    }

    private static long normalize(int value) {
        if (value >= 0) {
            return value;
        }
        return (~0L >>> 32) & value;
    }

    /**
     * Returns the object as a byte array, including header, padding and all fields.
     *
     * @param obj
     * @return
     */
    public static byte[] toByteArray(Object obj) throws Throwable {
        int len = (int) sizeOf(obj);
        byte[] bytes = new byte[len];
        MyUnsafe.copyMemory(obj, 0, bytes, MyUnsafe.ARRAY_BYTE_BASE_OFFSET, bytes.length);
        return bytes;
    }

    /**
     * Prints out the object (including header, padding, and all fields) as hex.
     *
     * <p>Some examples:
     *
     * <p><pre>
     * /**
     *  * Longs are always 8 byte aligned, so 4 bytes of padding
     *  * 0x00000000: 01 00 00 00 00 00 00 00  9B 81 61 DF 00 00 00 00
     *  * 0x00000010: EF CD AB 89 67 45 23 01
     *  *&#47;
     * static class Class8 {
     *   long l = 0x0123456789ABCDEFL;
     * }
     *
     * /**
     *  * 0x00000000: 01 00 00 00 00 00 00 00  8A BF 62 DF 67 45 23 01
     *  *&#47;
     * static class Class4 {
     *   int i = 0x01234567;
     * }
     *
     * /**
     *  * 0x00000000: 01 00 00 00 00 00 00 00  28 C0 62 DF 34 12 00 00
     *  *&#47;
     * static class Class2 {
     *   short s = 0x01234;
     * }
     *
     * /**
     *  * 0x00000000: 01 00 00 00 00 00 00 00  E3 C0 62 DF 12 00 00 00
     *  *&#47;
     * static class Class1 {
     *   byte b = 0x12;
     * }
     *
     * /**
     *  * 0x00000000: 01 00 00 00 00 00 00 00  96 C1 62 DF 12 00 00 00
     *  * 0x00000010: EF CD AB 89 67 45 23 01
     *  *&#47;
     * static class ClassMixed18 {
     *   byte b = 0x12;
     *   long l = 0x0123456789ABCDEFL;
     * }
     *
     * /**
     *  * 0x00000000: 01 00 00 00 00 00 00 00  4C C2 62 DF 12 00 00 00
     *  * 0x00000010: EF CD AB 89 67 45 23 01
     *  *&#47;
     * static class ClassMixed81 {
     *   long l = 0x0123456789ABCDEFL;
     *   byte b = 0x12;
     * }
     *
     * public static void printMemoryLayout() {
     *   UnsafeHelper.hexDump(System.out, new Class8());
     *   UnsafeHelper.hexDump(System.out, new Class4());
     *   UnsafeHelper.hexDump(System.out, new Class2());
     *   UnsafeHelper.hexDump(System.out, new Class1());
     *   UnsafeHelper.hexDump(System.out, new ClassMixed18());
     *   UnsafeHelper.hexDump(System.out, new ClassMixed81());
     * }
     * </pre>
     *
     * @param out PrintStream to print the hex to
     * @param obj The object to print
     */
    public static void hexDump(PrintStream out, Object obj) throws Throwable {
        // TODO Change this to use hexDumpAddress instead of toByteArray
        byte[] bytes = toByteArray(obj);
        hexDumpBytes(out, 0, bytes);
    }

    public static String hexDump(Object obj) throws Throwable {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        hexDump(new PrintStream(baos), obj);

        return baos.toString(StandardCharsets.UTF_8);
    }

    @SuppressWarnings("PointlessArithmeticExpression")
    public static void hexDumpBytes(PrintStream out, long offset, byte[] bytes) {
        final int lineWidth = 16;
        char[] line = new char[lineWidth * 3];

        for (int i = 0; i < bytes.length; i += lineWidth) {
            int len = Math.min(bytes.length - i, 16);

            for (int j = 0; j < len; j++) {
                int value = bytes[i + j] & 0xFF;
                line[j * 3 + 0] = hexArray[value >>> 4];
                line[j * 3 + 1] = hexArray[value & 0x0F];
                line[j * 3 + 2] = ' ';
            }

            int len1 = Math.min(len, 8) * 3;
            int len2 = Math.min(len - 8, 8) * 3;
            out.printf("0x%08X: %s %s%n", offset + (long) i, new String(line, 0, len1),
                    new String(line, 8 * 3, len2));
        }
    }

    public static void hexDumpAddress(PrintStream out, long address, long length) throws Throwable {
        byte[] bytes = new byte[16];
        while (length > 0) {
            long chunk = Math.min(bytes.length, length);
            MyUnsafe.copyMemory(null, address, bytes, MyUnsafe.ARRAY_BYTE_BASE_OFFSET, chunk);
            hexDumpBytes(out, address, bytes);
            length -= chunk;
            address += chunk;
        }
    }
}
